/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package kale.ui.component;

import kale.ui.ResourceTable;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.StackLayout;
import ohos.agp.components.Text;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.ThreeDimView;
import ohos.agp.utils.Color;
import ohos.agp.utils.Matrix;
import ohos.agp.utils.Rect;
import ohos.agp.utils.TextAlignment;
import ohos.app.Context;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.multimodalinput.event.TouchEvent;
import java.util.ArrayDeque;
import java.util.BitSet;
import java.util.Deque;
import java.util.HashMap;
import java.util.Locale;

/**
 * 3D view
 *
 * @since 2021-05-20
 */
public class ScalpelFrameLayout extends StackLayout {
    private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00201, "MY_TAG");
    private static final int INVALID_POINTER_ID = -1; // todo
    private static final int TRACKING_UNKNOWN = 0;
    private static final int TRACKING_VERTICALLY = 1;
    private static final int TRACKING_HORIZONTALLY = -1;
    private static final int ROTATION_MAX = 50;
    private static final int ROTATION_MIN = -ROTATION_MAX;
    private static final int ROTATION_DEFAULT_X = -10;
    private static final int ROTATION_DEFAULT_Y = 15;
    private static final float ZOOM_DEFAULT = 0.6f;
    private static final float ZOOM_MIN = 0.33f;
    private static final float ZOOM_MAX = 2f;
    private static final int SPACING_DEFAULT = 25;
    private static final int SPACING_MIN = 10;
    private static final int SPACING_MAX = 100;
    private static final int CHROME_COLOR = 0xFF888888;
    private static final int CHROME_SHADOW_COLOR = 0xFF000000;
    private static final int TEXT_OFFSET_DP = 2;
    private static final int TEXT_SIZE_DP = 10;
    private static final int CHILD_COUNT_ESTIMATION = 25;
    private static final boolean IS_DEBUG = false;
    private float rotateY = 0;
    private float rotateX = 0;
    private final int numNinety = 90;
    private final int numTwo = 2;
    private final float numfTwo = 2f;
    private final int numOneHundred = 100;
    private final int numNegativeOne = -1;

    /**
     * LayeredView
     *
     * @since 2021-05-20
     */
    private static class LayeredView {
        private static final int LAYER = -1;
        Component view;
        String text;
        int layer;

        void set(Component vw, int laye) {
            this.view = vw;
            this.layer = laye;
            if (vw.findComponentById(ResourceTable.Id_text_pager)!=null) {
                text = ((Text) vw.findComponentById(ResourceTable.Id_text_pager)).getText();
            }
        }

        void clear() {
            view = null;
            layer = LAYER;
        }
    }

    private final Rect viewBoundsRect = new Rect();
    private final Paint viewBorderPaint = new Paint();
    private final ThreeDimView camera = new ThreeDimView();
    private final Matrix matrix = new Matrix();
    private final int size2 = 2;
    private int[] location = new int[size2];
    private final BitSet visibilities = new BitSet(CHILD_COUNT_ESTIMATION);
    private final HashMap<Integer, String> idNames = new HashMap<>();
    private final Deque<LayeredView> layeredViewQueue = new ArrayDeque<>();
    private final Pool<LayeredView> layeredViewPool = new Pool<LayeredView>(CHILD_COUNT_ESTIMATION) {
        @Override
        protected LayeredView newObject() {
            return new LayeredView();
        }
    };

    private final float density;
    private final float slop;
    private final float textOffset;
    private final int textSize;

    private boolean isEnabled;
    private boolean isDrawViews = true;
    private boolean isDrawIds;
    private int pointerOne = INVALID_POINTER_ID;
    private float lastOneX;
    private float lastOneY;
    private int pointerTwo = INVALID_POINTER_ID;
    private float lastTwoX;
    private float lastTwoY;
    private int multiTouchTracking = TRACKING_UNKNOWN;

    private float rotationY = ROTATION_DEFAULT_Y;
    private float rotationX = ROTATION_DEFAULT_X;
    private float zoom = ZOOM_DEFAULT;
    private float spacing = SPACING_DEFAULT;

    private int chromeColor;
    private int chromeShadowColor;

    /** 构造
     * @param context
     */
    public ScalpelFrameLayout(Context context) {
        this(context, null);
    }

    /** 构造
     *
     * @param context
     * @param attrs
     */
    public ScalpelFrameLayout(Context context, AttrSet attrs) {
        this(context, attrs, "");
    }

    /** 构造
     *
     * @param context
     * @param attrs
     * @param styleName
     */
    public ScalpelFrameLayout(Context context, AttrSet attrs, String styleName) {
        super(context, attrs, styleName);
        final float numone = 1.5f;
        final int numtwo = 2;
        density = numone;
        slop = numtwo;
        textSize = (int) (TEXT_SIZE_DP * density);
        textOffset = TEXT_OFFSET_DP * density;
        setChromeColor(CHROME_COLOR);
        viewBorderPaint.setStyle(Paint.Style.STROKE_STYLE);
        viewBorderPaint.setTextSize(textSize);
        setChromeShadowColor(CHROME_SHADOW_COLOR);
        setTouchEventListener(new TouchListener());
        addDrawTask(new DrawTask());
    }

    /**
     * Set the view border chrome color.
     *
     * @param color
     */
    public void setChromeColor(int color) {
        if (chromeColor != color) {
            viewBorderPaint.setColor(new Color(color));
            chromeColor = color;
            invalidate();
        }
    }

    /**
     * Get the view border chrome color.
     *
     * @return color
     */
    public int getChromeColor() {
        return chromeColor;
    }

    /**
     * Set the view border chrome shadow color.
     *
     * @param color
     */
    public void setChromeShadowColor(int color) {
        if (chromeShadowColor != color) {
            chromeShadowColor = color;
            invalidate();
        }
    }

    /**
     * Get the view border chrome shadow color.
     *
     * @return color
     */
    public int getChromeShadowColor() {
        return chromeShadowColor;
    }

    /**
     * Set whether or not the 3D view layer interaction is enabled.
     *
     * @param isEenabled
     */
    public void setLayerInteractionEnabled(boolean isEenabled) {
        if (this.isEnabled != isEenabled) {
            this.isEnabled = isEenabled;
            invalidate();
        }
    }

    /**
     * Returns true when 3D view layer interaction is enabled.
     *
     * @return enabled
     */
    public boolean isLayerInteractionEnabled() {
        return isEnabled;
    }

    /**
     * Set whether the view layers draw their contents. When false, only wireframes are shown.
     *
     * @param isDdrawViews
     */
    public void setDrawViews(boolean isDdrawViews) {
        if (this.isDrawViews != isDdrawViews) {
            this.isDrawViews = isDdrawViews;
            invalidate();
        }
    }

    /**
     * Returns true when view layers draw their contents.
     *
     * @return drawViews
     */
    public boolean isDrawingViews() {
        return isDrawViews;
    }

    /**
     * Set whether the view layers draw their IDs.
     *
     * @param isDdrawIds
     */
    public void setDrawIds(boolean isDdrawIds) {
        if (this.isDrawIds != isDdrawIds) {
            this.isDrawIds = isDdrawIds;
            invalidate();
        }
    }

    /**
     * Returns true when view layers draw their IDs.
     *
     * @return drawIds
     */
    public boolean isDrawingIds() {
        return isDrawIds;
    }

    /**
     * TouchListener
     *
     * @author AnBetter
     * @since 2021-06-01
     */
    class TouchListener implements TouchEventListener {
        @Override
        public boolean onTouchEvent(Component component, TouchEvent event) {
            if (isEnabled) {
                int action = event.getAction();
                switch (action) {
                    case TouchEvent.PRIMARY_POINT_DOWN:
                    case TouchEvent.OTHER_POINT_DOWN:
                        int index = (action == TouchEvent.PRIMARY_POINT_DOWN) ? 0 : event.getIndex();
                        if (pointerOne == INVALID_POINTER_ID) {
                            pointerOne = event.getPointerId(index);
                            lastOneX = event.getPointerPosition(index).getX();
                            lastOneY = event.getPointerPosition(index).getY();
                        } else if (pointerTwo == INVALID_POINTER_ID) {
                            pointerTwo = event.getPointerId(index);
                            lastTwoX = event.getPointerPosition(index).getX();
                            lastTwoY = event.getPointerPosition(index).getY();
                        }
                        break;
                    case TouchEvent.POINT_MOVE:
                        if (event.getPointerCount() == 1) {
                            setPointMove(event);
                        } else {
                            setPointMove2(event);
                        }
                        break;
                    case TouchEvent.CANCEL:
                    case TouchEvent.PRIMARY_POINT_UP:
                    case TouchEvent.OTHER_POINT_UP: {
                        int index2 = (action != TouchEvent.PRIMARY_POINT_UP) ? 0 : event.getIndex();
                        int pointerId = event.getPointerId(index2);
                        if (pointerOne == pointerId) {
                            pointerOne = pointerTwo;
                            lastOneX = lastTwoX;
                            lastOneY = lastTwoY;
                            pointerTwo = INVALID_POINTER_ID;
                            multiTouchTracking = TRACKING_UNKNOWN;
                        } else if (pointerTwo == pointerId) {
                            pointerTwo = INVALID_POINTER_ID;
                            multiTouchTracking = TRACKING_UNKNOWN;
                        }
                        break;
                    }
                    default:
                        break;
                }
                return true;
            }
            return false;
        }
    }

    private void setPointMove(TouchEvent event) {
        for (int indexJ = 0, count = event.getPointerCount(); indexJ < count; indexJ++) {
            if (pointerOne == event.getPointerId(indexJ)) {
                float eventX = event.getPointerPosition(indexJ).getX();
                float eventY = event.getPointerPosition(indexJ).getY();
                float dx = eventX - lastOneX;
                float dy = eventY - lastOneY;
                float drx = numNinety * (dx / getWidth());
                float dry = numNinety * (-dy / getHeight()); // Invert Y-axis.
                float tempY = Math.min(Math.max(rotateY + drx, ROTATION_MIN), ROTATION_MAX);
                rotationY = tempY - rotateY;
                rotateY = tempY;
                float tempX = Math.min(Math.max(rotateX + dry, ROTATION_MIN), ROTATION_MAX);
                rotationX = tempX - rotateX;
                rotateX = tempX;
                lastOneX = eventX;
                lastOneY = eventY;
                invalidate();
            }
        }
    }

    private void setPointMove2(TouchEvent event) {
        float mXone = event.getPointerPosition(pointerOne).getX();
        float mYone = event.getPointerPosition(pointerOne).getY();
        float mXtwo = event.getPointerPosition(pointerTwo).getX();
        float mYtwo = event.getPointerPosition(pointerTwo).getY();

        float dxOne = mXone - lastOneX;
        float dyOne = mYone - lastOneY;
        float dxTwo = mXtwo - lastTwoX;
        float dyTwo = mYtwo - lastTwoY;

        if (multiTouchTracking == TRACKING_UNKNOWN) {
            float adx = Math.abs(dxOne) + Math.abs(dxTwo);
            float ady = Math.abs(dyOne) + Math.abs(dyTwo);

            if (adx > slop * numTwo || ady > slop * numTwo) {
                if (adx > ady) {
                    multiTouchTracking = TRACKING_HORIZONTALLY;
                } else {
                    multiTouchTracking = TRACKING_VERTICALLY;
                }
            }
        }

        if (multiTouchTracking == TRACKING_VERTICALLY) {
            if (mYone >= mYtwo) {
                zoom += dyOne / getHeight() - dyTwo / getHeight();
            } else {
                zoom += dyTwo / getHeight() - dyOne / getHeight();
            }

            zoom = Math.min(Math.max(zoom, ZOOM_MIN), ZOOM_MAX);
            invalidate();
        } else if (multiTouchTracking == TRACKING_HORIZONTALLY) {
            if (mXone >= mXtwo) {
                spacing += (dxOne / getWidth() * SPACING_MAX) - (dxTwo / getWidth() * SPACING_MAX);
            } else {
                spacing += (dxTwo / getWidth() * SPACING_MAX) - (dxOne / getWidth() * SPACING_MAX);
            }

            spacing = Math.min(Math.max(spacing, SPACING_MIN), SPACING_MAX);
            invalidate();
        }

        if (multiTouchTracking != TRACKING_UNKNOWN) {
            lastOneX = mXone;
            lastOneY = mYone;
            lastTwoX = mXtwo;
            lastTwoY = mYtwo;
        }
    }

    /**
     * DrawTask
     *
     * @author AnBetter
     * @since 2021-06-01
     */
    class DrawTask implements Component.DrawTask {
        @Override
        public void onDraw(Component component, Canvas canvas) {
            if (!isEnabled) {
                return;
            }
            int saveCount = canvas.save();
            canvas.save();
            camera.rotateX(rotationX);
            camera.rotateY(rotationY);
            camera.getMatrix(matrix);
            camera.applyToCanvas(canvas);
            canvas.restore();
            float cx = getWidth() / numfTwo;
            float cy = getHeight() / numfTwo;
            matrix.preTranslate(-cx, -cy);
            matrix.postTranslate(cx, cy);
            canvas.concat(matrix);
            canvas.scale(zoom, zoom, cx, cy);

            if (!layeredViewQueue.isEmpty()) {
                throw new AssertionError("View queue is not empty.");
            }
            layeredViewQueue.clear();
            for (int index = 0, count = getChildCount(); index < count; index++) {
                LayeredView layeredView = layeredViewPool.obtain();
                layeredView.set(getComponentAt(index), 0);
                layeredViewQueue.add(layeredView);
            }

            location = getLocationOnScreen();
            float mScreenX = location[0];
            float mScreenY = location[1];
            setCanvas(canvas,mScreenX,mScreenY);
            canvas.restoreToCount(saveCount);
        }
    }

    private void setCanvas(Canvas canvas,float screenX,float screenY) {
        LayeredView layeredView = layeredViewQueue.pollFirst();
        ComponentContainer componentContainer = (ComponentContainer)
                ((ComponentContainer)layeredView.view).getComponentAt(0);

        for (int index = 0; index < componentContainer.getChildCount(); index++) {
            LayeredView childLayeredView = layeredViewPool.obtain();
            childLayeredView.set(componentContainer.getComponentAt(index), index);
            layeredViewQueue.add(childLayeredView);
        }

        while (!layeredViewQueue.isEmpty()) {
            layeredView = layeredViewQueue.pollFirst();
            Component view = layeredView.view;
            layeredViewPool.restore(layeredView);
            int viewSaveCount = canvas.save();
            float translateShowX = rotationY / ROTATION_MAX;
            float translateShowY = rotationX / ROTATION_MAX;
            int layer = layeredView.layer;
            float tx = layer * spacing * density * translateShowX;
            float ty = layer * spacing * density * translateShowY;
            canvas.translate(tx, -ty);
            location = view.getLocationOnScreen();
            canvas.translate(location[0] - screenX, location[1] - screenY);
            viewBorderPaint.setTextSize(numOneHundred);
            viewBoundsRect.set(0, 0, view.getWidth(), view.getHeight());
            canvas.drawRect(viewBoundsRect, viewBorderPaint);
            viewBorderPaint.setTextAlign(TextAlignment.CENTER);

            // 绘制文字
            canvas.drawText(viewBorderPaint, layeredView.text,
                    viewBoundsRect.getCenterX(), viewBoundsRect.getCenterY());

            if (isDrawIds) {
                int id = view.getId();
                if (id != numNegativeOne) {
                    canvas.drawText(viewBorderPaint, nameForId(id), textOffset, textSize);
                }
            }
            canvas.restoreToCount(viewSaveCount);
        }
    }

    private String nameForId(int id) {
        String name = idNames.get(id);
        if (name == null) {
            Locale mLocalezh = new Locale("zh","CN");
            name = String.format(mLocalezh,"0x%8x", id);
            idNames.put(id, name);
        }
        return name;
    }

    /** Pool
     *
     * @param <T>
     * @since 2021-06-01
     */
    private abstract static class Pool<T> {
        private final Deque<T> pool;

        Pool(int initialSize) {
            pool = new ArrayDeque<>(initialSize);
            for (int index = 0; index < initialSize; index++) {
                pool.addLast(newObject());
            }
        }

        T obtain() {
            return pool.isEmpty() ? newObject() : pool.removeLast();
        }

        void restore(T instance) {
            pool.addLast(instance);
        }

        protected abstract T newObject();
    }
}

